﻿using System;
using System.Diagnostics;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Windows;
using System.Windows.Threading;
using System.Xml.Serialization;

namespace weBoard
{
    /// <summary>
    /// Network exchange class
    /// </summary>
    class weBoardClient
    {
        #region Private variables

        private TcpClient     _tcpClient;

        private NetworkStream _netStream = default(NetworkStream);

        private mainWnd _wnd;

        private delegate int handler(bool state);

        private handler _handler;

        private String _nick = "";

        private String _host = "";

        private Int32 _port;

        #endregion Private variables

        #region Public properties
        /// <summary>
        /// Connection state
        /// </summary>
        public bool Connected    { get; private set; }

        /// <summary>
        /// For threading purposes
        /// </summary>
        public bool WasConnected { get; private set; }

        /// <summary>
        /// Indicates whenever server discover attemp is Successful
        /// </summary>
        public bool Discovered  { get; private set; }

        /// <summary>
        /// Gets weBoardClient thread
        /// </summary>
        public Thread GetThread { get; private set; }

        #endregion Public properties

        #region Private members

        private void WriteString(string msg, NetworkStream net)
        {
            var data = Encoding.ASCII.GetBytes(msg);
            var sz = BitConverter.GetBytes(data.Length);
            net.Write(sz, 0, sz.Length);
            net.Write(data, 0, data.Length);
            net.Flush();
        }

        private void WriteData(byte[] msg, NetworkStream net)
        {
            var sz = BitConverter.GetBytes(msg.Length);
            net.Write(sz, 0, sz.Length);
            net.Write(msg, 0, msg.Length);
            net.Flush();
        }

        private byte[] ReadData(NetworkStream net)
        {
            var sz = new Byte[sizeof(Int32)];
            net.Read(sz, 0, sz.Length);
            var data = new Byte[BitConverter.ToInt32(sz, 0)];
            net.Read(data, 0, data.Length);
            return data;
        }

        /// <summary>
        /// Sends command protocol messages (i.e. connect, disconnect, discover etc...)
        /// </summary>
        /// <param name="msg">Message to send</param>
        private void SendMsg(string msg)
        {
            if (!Connected)
                return;
            try
            {
                WriteString("#" + msg + ":" + _nick + "$", _netStream);
            }

            catch (StackOverflowException)
            {
                Debug.WriteLine("SendMsg::StackOverflowException. Unable to send message: " + msg);
            }

            catch (SocketException)
            {
                Debug.WriteLine("SendMsg::SocketException. Unable to send message: " + msg);
            }
            catch (IOException)
            {
                Debug.WriteLine("SendMsg::IOException. Unable to send message: " + msg);
            }
            catch (Exception)
            {
                Debug.WriteLine("SendMsg::Exception occured: " + msg);
                Disconnect();
            }
        }

        /// <summary>
        /// Update gui when disconnected
        /// </summary>
        private void GuiDisconnect()
        {
            if (_handler != null)
                _handler(false);
        }
        
        private bool Dispatch(string msg)
        {
            if (msg.Contains("#DSCALL:SV"))
            {
                Debug.WriteLine("Server stopped...");
                return false;
            }
            if (msg.Contains("#NICKDUP:SV"))
            {
                Debug.WriteLine("Server said that nick " + _nick + " already exists...");
                MessageBox.Show("Server said that nick " + _nick + " already exists...", "Message from server",
                    MessageBoxButton.OK, MessageBoxImage.Information);
                return false;
            }
            if (msg.Contains("#WEBRD:SV"))
            {
                Discovered = true;
                Debug.WriteLine("Server said that it is weBoardServer");
                return false;
            }
            Debug.WriteLine(msg);//default
            return true;//true if we may continue processing, the meaning of false is "needs disconnect"
        }

        private T ByteArrayToObj<T>(byte[] obj)
        {
            using (var ms = new MemoryStream(obj))
                return (T)(new XmlSerializer(typeof(T))).Deserialize(ms);
        }

        private byte[] ObjToByteArray<T>(T obj)
        {
            using (var ms = new MemoryStream())
            {
                var xmlS = new XmlSerializer(typeof(T));
                xmlS.Serialize(ms, obj);
                return ms.ToArray();
            }
        }

        /// <summary>
        /// Draws received data with main drawbot
        /// </summary>
        /// <param name="data">Deserialized data</param>
        private void UnpackAction(byte[] data)
        {
            try
            {
                if (Connected)
                    _wnd.Dispatcher.Invoke(new Action(() => _wnd.Drawbot.Action(ByteArrayToObj<weBoardAction>(data))));
            }
            catch (Exception ex)
            {
                Debug.WriteLine("UnpackAction::Invalid data or drawing bot fail. Exception is:" + ex.Message);
            }
        }

        /// <summary>
        /// Listening thread
        /// </summary>
        private void GetMsg()
        {
            try
            {
                while (Connected)
                {
                    _netStream = _tcpClient.GetStream();
                    var data = ReadData(_netStream);
                    if (data[0].Equals(0x1f) && data[1].Equals(0x8b))//gzip
                    {
                        //here is decompression, +compression in other plases, please!
                        //if (msg.StartsWith("<?xml") && msg.EndsWith("</ArrayOfWeBoardAction>"))
                        UnpackAction(data);
                        continue;
                    }
                    var msg = Encoding.ASCII.GetString(data);
                    if (msg.StartsWith("<?xml") && (msg.EndsWith("</weBoardAction>") || msg.EndsWith("</ArrayOfWeBoardAction>")))
                    {
                        UnpackAction(data);
                        continue;
                    }
                    if (msg.StartsWith("#") && msg.EndsWith("$"))
                    {
                        msg = msg.Substring(0, msg.IndexOf('$'));
                        if (!Dispatch(msg))
                            break;
                    }
                }
                Disconnect();
            }
            catch (Exception)
            {
                Disconnect();
            }
        }
        #endregion Private members

        #region Public members

        /// <summary>
        /// Disconnect from server
        /// </summary>
        public void Disconnect()
        {
            if (!Connected)
                return;
            try
            {
                Application.Current.Dispatcher.BeginInvoke(new Action(GuiDisconnect), DispatcherPriority.Normal);
                SendMsg("DSC");
                Connected = false;
                _netStream.Close();
                _tcpClient.Close();
                Debug.WriteLine("Disconnect::" + _nick + " disconnected from " + _host + ":" + _port);
                GetThread.Abort();
            }
            catch (Exception ex)
            {
                Debug.WriteLine("Disconnect::Exception::" + ex.Message);
            }
        }

        /// <summary>
        /// Connect to server with nick that should be unique
        /// </summary>
        /// <param name="nick">Nickname</param>
        /// <param name="host">Server address</param>
        /// <param name="port">Server port</param>
        /// <param name="func">Delegate to enable evential GUI update</param>
        public void Connect(string nick, string host, Int32 port, Func<bool, int> func, mainWnd window)
        {
            try
            {
                _wnd = window;
                _handler = new handler(func);
                if (nick == String.Empty || nick.IndexOfAny(new []{'$', ':'}) >= 0 || nick.Length < 3)
                    throw new ArgumentOutOfRangeException("Invalid nickname");
                _nick = nick;
                _host = host;
                _port = port;
                Debug.WriteLine("Connect::" + _nick + " connecting to " + _host + ":" + _port);
                _tcpClient = new TcpClient();
                _tcpClient.Connect(_host, _port);
                _netStream = _tcpClient.GetStream();
                Connected = true;
                WasConnected = true;
                SendMsg("CNN");
                GetThread = new Thread(GetMsg) {Name = "GetMsg_Thread"};
                GetThread.SetApartmentState(ApartmentState.STA);
                GetThread.Start();
                Debug.WriteLine("Connect::" + _nick + " connected to " + _host + ":" + _port);
            }
            catch (Exception ex)
            {
                Disconnect();
                Debug.WriteLine(ex.Message);
                Debug.WriteLine("Connect::It's seems that server is down...");
                if (_nick != "#SEARCH")
                    MessageBox.Show(ex.Message, "Connection attempt failed", MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }

        /// <summary>
        /// Sends Action to server
        /// </summary>
        /// <param name="act">Action to send</param>
        public void SendActionMsg(weBoardAction act)
        {
            if (!Connected)
                return;
            try
            {
                WriteData(ObjToByteArray(act), _netStream);
            }
            catch (Exception ex)
            {
                Debug.WriteLine("SendActionMsg::Exception::" + ex.Message);
            }
        }
        #endregion Public members
    }
}